package gateway

import (
	"context"
	"fmt"
	"github.com/gogo/protobuf/proto"
	"github.com/hyperledger/fabric/common/configtx"
	commonutil "github.com/hyperledger/fabric/common/util"
	"github.com/hyperledger/fabric/core/scc/cscc"
	"github.com/hyperledger/fabric/core/scc/qscc"
	cb "github.com/hyperledger/fabric/protos/common"
	peerproto "github.com/hyperledger/fabric/protos/peer"
	"github.com/hyperledger/fabric/protos/utils"
	protoutil "github.com/hyperledger/fabric/protos/utils"
	"io/ioutil"
	"time"
)

func createChannel(suite CryptoSuite, ordererCli *ordererClient, channelID, channelTxFile string) error {
	chConfigEnv, err := createChannelFromConfigTx(channelTxFile)
	if err != nil {
		return fmt.Errorf("create channel from configtx fail:%v", err)
	}
	chConfigEnv, err = sanityCheckAndSignConfigTx(channelID, chConfigEnv, suite)
	if err != nil {
		return fmt.Errorf("fail to sanity checkand sign config tx:%v", err)
	}
	broadcastCli, err := ordererCli.GetBroadcast()
	if err != nil {
		return err
	}
	defer broadcastCli.Close()
	err = broadcastCli.Send(chConfigEnv)
	if err != nil {
		return fmt.Errorf("fail to broadcast creat channel envelop: %v", err)
	}
	_, err = getGenesisBlock(suite, ordererCli, channelID)
	if err != nil {
		fmt.Errorf("fail to get genesis block:%v", err)
	}
	return nil
}

func joinChannel(suite CryptoSuite, ordererCli *ordererClient, channelID string, peerClients []*peerClient) error {
	block, err := getGenesisBlock(suite, ordererCli, channelID)
	if err != nil {
		return err
	}
	input := &peerproto.ChaincodeInput{Args: [][]byte{[]byte(cscc.JoinChain), block}}
	spec := &peerproto.ChaincodeSpec{
		Type:        peerproto.ChaincodeSpec_Type(peerproto.ChaincodeSpec_Type_value["GOLANG"]),
		ChaincodeId: &peerproto.ChaincodeID{Name: "cscc"},
		Input:       input,
	}
	signer, err := suite.NewSigner()
	if err != nil {
		return err
	}
	invocation := &peerproto.ChaincodeInvocationSpec{ChaincodeSpec: spec}
	creator, err := signer.Serialize()
	if err != nil {
		return err
	}
	prop, _, err := protoutil.CreateProposalFromCIS(cb.HeaderType_CONFIG, "", invocation, creator)
	if err != nil {
		return fmt.Errorf("Error creating proposal for join: %v", err)
	}
	signedProp, err := GetSignedProposal(prop, signer)
	if err != nil {
		return fmt.Errorf("Error creating signed proposal:%v", err)
	}
	// send transaction to peer client
	for _, client := range peerClients {
		endorser, err := client.Endorser()
		if err != nil {
			return fmt.Errorf("GetEndorser:%v", err)
		}
		resp, err := endorser.ProcessProposal(context.Background(), signedProp)
		if resp != nil {
			if resp.Response != nil && resp.Response.Status != 200 {
				return fmt.Errorf("invliad response status:%d, msg:%s", resp.Response.Status, resp.Response.Message)
			}
			fmt.Println("success join channel")
		} else {
			return fmt.Errorf("response error: nil response")
		}
	}
	return nil
}

// list channels of consortium network
func listChannel(suite CryptoSuite, peerCli *peerClient) ([]string, error) {
	invocation := &peerproto.ChaincodeInvocationSpec{
		ChaincodeSpec: &peerproto.ChaincodeSpec{
			Type:        peerproto.ChaincodeSpec_Type(peerproto.ChaincodeSpec_Type_value["GOLANG"]),
			ChaincodeId: &peerproto.ChaincodeID{Name: "cscc"},
			Input:       &peerproto.ChaincodeInput{Args: [][]byte{[]byte(cscc.GetChannels)}},
		},
	}

	signer, err := suite.NewSigner()
	if err != nil {
		return nil, err
	}
	creator, err := signer.Serialize()
	if err != nil {
		return nil, err
	}
	prop, _, err := protoutil.CreateProposalFromCIS(cb.HeaderType_ENDORSER_TRANSACTION, "", invocation, creator)
	if err != nil {
		return nil, err
	}
	signedProp, err := GetSignedProposal(prop, signer)
	if err != nil {
		return nil, err
	}

	endorser, err := peerCli.Endorser()
	if err != nil {
		return nil, err
	}
	resp, err := endorser.ProcessProposal(context.TODO(), signedProp)
	if err != nil {
		return nil, err
	}
	var channelQueryResp peerproto.ChannelQueryResponse
	err = proto.Unmarshal(resp.Response.Payload, &channelQueryResp)
	if err != nil {
		return nil, fmt.Errorf(fmt.Sprintf("Cannot read channels list response, %s", err))
	}
	var channels []string
	for _, channel := range channelQueryResp.Channels {
		channels = append(channels, channel.ChannelId)
	}
	return channels, nil
}

// fetch block from fabric network via orderer client
func fetchBlockFromChannel(suite CryptoSuite, ordererCli *ordererClient, channelID string, blockNum uint64) (*cb.Block, error) {
	signer, err := suite.NewSigner()
	if err != nil {
		return nil, err
	}

	deliver, err := ordererCli.GetDeliverClient(channelID, signer)
	if err != nil {
		return nil, err
	}

	return deliver.getSpecifiedBlock(blockNum)
}

func fetchNewestBlockFromChannel(suite CryptoSuite, ordererCli *ordererClient, channelID string) (*cb.Block, error) {
	signer, err := suite.NewSigner()
	if err != nil {
		return nil, err
	}

	deliver, err := ordererCli.GetDeliverClient(channelID, signer)
	if err != nil {
		return nil, err
	}
	return deliver.getMewestBlock()
}

func getBlockInfoFromChannel(suite CryptoSuite, peerCli *peerClient, channelID string) (*cb.BlockchainInfo, error) {
	invocation := &peerproto.ChaincodeInvocationSpec{
		ChaincodeSpec: &peerproto.ChaincodeSpec{
			Type:        peerproto.ChaincodeSpec_Type(peerproto.ChaincodeSpec_Type_value["GOLANG"]),
			ChaincodeId: &peerproto.ChaincodeID{Name: "qscc"},
			Input:       &peerproto.ChaincodeInput{Args: [][]byte{[]byte(qscc.GetChainInfo), []byte(channelID)}},
		},
	}

	var prop *peerproto.Proposal
	signer, _ := suite.NewSigner()
	creator, _ := signer.Serialize()
	prop, _, err := utils.CreateProposalFromCIS(cb.HeaderType_ENDORSER_TRANSACTION, "", invocation, creator)
	if err != nil {
		return nil, fmt.Errorf("fail to create proposal from CIS:%v", err)
	}
	signedProp, err := GetSignedProposal(prop, signer)
	if err != nil {
		return nil, fmt.Errorf("fail to get signed proposal:%v", err)
	}

	endorser, err := peerCli.Endorser()
	if err != nil {
		return nil, fmt.Errorf("fail to creat endorser client:%v", err)
	}

	resp, err := endorser.ProcessProposal(context.Background(), signedProp)
	if err != nil {
		return nil, fmt.Errorf("fail to process proposal:%v", err)
	}

	if resp.Response == nil || resp.Response.Status != 200 {
		return nil, fmt.Errorf("received bad response, status %d: %s", resp.Response.Status, resp.Response.Message)
	}

	blockInfo := &cb.BlockchainInfo{}
	err = proto.Unmarshal(resp.Response.Payload, blockInfo)
	if err != nil {
		return nil, fmt.Errorf("cannot read qscc response:%v", err)
	}
	return blockInfo, nil
}
func createChannelFromConfigTx(configTxFile string) (*cb.Envelope, error) {
	cftxBytes, err := ioutil.ReadFile(configTxFile)
	if err != nil {
		return nil, fmt.Errorf("Not Found channel configtx file:%v ", err)
	}
	return utils.UnmarshalEnvelope(cftxBytes)
}

func sanityCheckAndSignConfigTx(channelID string, updateConfig *cb.Envelope, suite CryptoSuite) (*cb.Envelope, error) {
	payload, err := utils.ExtractPayload(updateConfig)
	if err != nil {
		return nil, fmt.Errorf("bad payload:%v", err)
	}
	if payload.Header == nil || payload.Header.ChannelHeader == nil {
		return nil, fmt.Errorf("bad header:%v", err)
	}
	chEnv, err := utils.UnmarshalChannelHeader(payload.Header.ChannelHeader)
	if err != nil {
		return nil, err
	}
	if chEnv.Type != int32(cb.HeaderType_CONFIG_UPDATE) {
		return nil, fmt.Errorf("bad type:%v", err)
	}
	if chEnv.ChannelId == "" {
		return nil, fmt.Errorf("empty channel id")
	}
	if chEnv.ChannelId != channelID {
		return nil, fmt.Errorf(fmt.Sprintf("mismatched channel ID %s != %s", chEnv.ChannelId, channelID))
	}

	updateConfigEnv, err := configtx.UnmarshalConfigUpdateEnvelope(payload.Data)
	if err != nil {
		return nil, err
	}
	signer, err := suite.NewSigner()
	if err != nil {
		return nil, err
	}
	sigHeader, err := signer.NewSignatureHeader()
	if err != nil {
		return nil, err
	}
	configSig := &cb.ConfigSignature{
		SignatureHeader: utils.MarshalOrPanic(sigHeader),
	}

	configSig.Signature, err = signer.Sign(commonutil.ConcatenateBytes(configSig.SignatureHeader, updateConfigEnv.ConfigUpdate))
	if err != nil {
		return nil, err
	}
	updateConfigEnv.Signatures = append(updateConfigEnv.Signatures, configSig)
	return utils.CreateSignedEnvelope(cb.HeaderType_CONFIG_UPDATE, channelID, signer, updateConfigEnv, 0, 0)
}

func getGenesisBlock(suite CryptoSuite, ordererCli *ordererClient, channelID string) ([]byte, error) {
	signer, err := suite.NewSigner()
	if err != nil {
		return nil, err
	}
	deliverCli, err := ordererCli.GetDeliverClient(channelID, signer)
	if err != nil {
		return nil, err
	}

	timer := time.NewTimer(10 * time.Second)
	defer timer.Stop()
	for {
		select {
		case <-timer.C:
			deliverCli.Close()
			return nil, fmt.Errorf("timeout waiting for channel creation")
		default:
			if block, err := deliverCli.getSpecifiedBlock(0); err != nil {
				deliverCli.Close()
				time.Sleep(200 * time.Millisecond)
				deliverCli, err = ordererCli.GetDeliverClient(channelID, signer)
				if err != nil {
					return nil, fmt.Errorf("fail to get deliever client:%v", err)
				}
			} else {
				deliverCli.Close()
				return proto.Marshal(block)
			}
		}
	}

}
